<?php
/**
 * Invitation model.
 *
 * @copyright 2009-2019 Vanilla Forums Inc.
 * @license GPL-2.0-only
 * @package Dashboard
 * @since 2.0
 */

/**
 * Handles invitation data.
 */
class InvitationModel extends Gdn_Model
{
    /**
     * Class constructor. Defines the related database table name.
     */
    public function __construct()
    {
        parent::__construct("Invitation");
    }

    /**
     *
     *
     * @param $invitationID
     * @return array|bool|stdClass
     */
    public function getByInvitationID($invitationID)
    {
        $dataSet = $this->SQL
            ->from("Invitation i")
            ->join("User su", "i.InsertUserID = su.UserID")
            ->join("User au", "i.AcceptedUserID = au.UserID", "left")
            ->select("i.*")
            ->select("au.UserID", "", "AcceptedUserID")
            ->select("au.Email", "", "AcceptedEmail")
            ->select("au.Name", "", "AcceptedName")
            ->select("su.UserID", "", "SenderUserID")
            ->select("su.Email", "", "SenderEmail")
            ->select("su.Name", "", "SenderName")
            ->where("i.InvitationID", $invitationID)
            ->get();
        return $dataSet->firstRow();
    }

    /**
     *
     *
     * @param $userID
     * @param string $invitationID
     * @param int $limit
     * @param int $offset
     * @param bool $joinUsers
     * @return Gdn_DataSet
     * @throws Exception
     */
    public function getByUserID($userID, $invitationID = "", $limit = 50, $offset = 0, $joinUsers = true)
    {
        $this->SQL->select("i.*")->from("Invitation i");

        if ($joinUsers) {
            $this->SQL->select("u.Name", "", "AcceptedName")->join("User u", "i.AcceptedUserID = u.UserID", "left");
        }

        $this->SQL
            ->where("i.InsertUserID", $userID)
            ->orderBy("i.DateInserted", "desc")
            ->limit($limit, $offset);

        if (is_numeric($invitationID)) {
            $this->SQL->where("Invitation.InvitationID", $invitationID);
        }

        return $this->SQL->get();
    }

    /**
     *
     *
     * @param array $formPostValues
     * @param array|bool $settings
     * @throws Exception
     * @return bool|array
     */
    public function save($formPostValues, $settings = false)
    {
        $session = Gdn::session();
        $userID = $session->UserID;
        $sendEmail = val("SendEmail", $settings, true);
        $resend = val("Resend", $settings, false);
        $returnRow = val("ReturnRow", $settings, false);

        // Define the primary key in this model's table.
        $this->defineSchema();

        // Add & apply any extra validation rules:
        $this->Validation->applyRule("Email", "Email");

        // Make sure required db fields are present.
        $this->addInsertFields($formPostValues);
        if (!isset($formPostValues["DateExpires"])) {
            $expires = strtotime(c("Garden.Registration.InviteExpiration"));
            if ($expires > time()) {
                $formPostValues["DateExpires"] = Gdn_Format::toDateTime($expires);
            }
        }

        $formPostValues["Code"] = $this->getInvitationCode();

        // Validate the form posted values
        if ($this->validate($formPostValues, true) === true) {
            $userModel = Gdn::userModel();
            $fields = $this->Validation->validationFields(); // All fields on the form that need to be validated
            $email = val("Email", $fields, "");

            // Make sure this user has a spare invitation to send.
            $inviteCount = $userModel->getInvitationCount($userID);
            if ($inviteCount == 0) {
                $this->Validation->addValidationResult("Email", "You do not have enough invitations left.");
                return false;
            }

            // Make sure that the email does not already belong to an account in the application.
            $testData = $userModel->getWhere(["Email" => $email]);
            if ($testData->numRows() > 0) {
                $this->Validation->addValidationResult(
                    "Email",
                    "The email you have entered is already related to an existing account."
                );
                return false;
            }

            // Make sure that the email does not already belong to an invitation in the application.
            $testData = $this->getWhere(["Email" => $email]);
            $deleteID = false;
            if ($testData->numRows() > 0) {
                if (!$resend) {
                    $this->Validation->addValidationResult(
                        "Email",
                        "An invitation has already been sent to the email you entered."
                    );
                    return false;
                } else {
                    // Mark the old invitation for deletion.
                    $deleteID = val("InvitationID", $testData->firstRow(DATASET_TYPE_ARRAY));
                }
            }

            // Define the fields to be inserted
            $fields = $this->Validation->schemaValidationFields();

            // Call the base model for saving
            $invitationID = parent::save($fields);

            // Delete an old invitation.
            if ($invitationID && $deleteID) {
                $this->delete($deleteID);
            }

            // Now that saving has succeeded, update the user's invitation settings
            if ($inviteCount > 0) {
                $userModel->reduceInviteCount($userID);
            }

            // And send the invitation email
            if ($sendEmail) {
                try {
                    $this->send($invitationID);
                } catch (Exception $ex) {
                    $this->Validation->addValidationResult(
                        "Email",
                        sprintf("Invitation email failed to send: %s", strip_tags($ex->getMessage()))
                    );
                    return false;
                }
            }

            if ($returnRow) {
                return (array) $this->getByInvitationID($invitationID);
            } else {
                return true;
            }
        }
        return false;
    }

    /**
     *
     *
     * @param $invitationID
     * @throws Exception
     */
    public function send($invitationID)
    {
        $invitation = $this->getByInvitationID($invitationID);
        $session = Gdn::session();
        if ($invitation === false) {
            throw new Exception(t("ErrorRecordNotFound"));
        } elseif ($session->UserID != $invitation->SenderUserID) {
            throw new Exception(t("InviteErrorPermission", t("ErrorPermission")));
        } else {
            // Some information for the email
            $registrationUrl = externalUrl(
                "entry/registerinvitation/{$invitation->Code}" .
                    (c("Garden.Registration.InviteTarget", "")
                        ? "?Target=" . c("Garden.Registration.InviteTarget", "")
                        : "")
            );

            $appTitle = Gdn::config("Garden.Title");
            $email = new Gdn_Email();
            $email->subject(sprintf(t("[%s] Invitation"), $appTitle));
            $email->to($invitation->Email);

            $emailTemplate = $email->getEmailTemplate();
            $message =
                t("Hello!") . " " . sprintf(t("%s has invited you to join %s."), $invitation->SenderName, $appTitle);

            $emailTemplate
                ->setButton($registrationUrl, t("Join this Community Now"))
                ->setMessage($message)
                ->setTitle(sprintf(t("Join %s"), $appTitle));

            $email->setEmailTemplate($emailTemplate);

            try {
                $email->send();
            } catch (Exception $e) {
                if (debug()) {
                    throw $e;
                }
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    public function delete($where = [], $options = [])
    {
        if (is_numeric($where)) {
            deprecated("InvitationModel->delete(int)", "InvitationModel->deleteID(int)");
            $result = $this->deleteID($where, $options);
            return $result;
        }

        parent::delete($where, $options);
    }

    /**
     * {@inheritdoc}
     */
    public function deleteID($id, $options = [])
    {
        \Webmozart\Assert\Assert::integerish($id);
        \Webmozart\Assert\Assert::isArray($options);

        $session = Gdn::session();
        $userID = $session->UserID;

        // Validate that this user can delete this invitation:
        $invitation = $this->getID($id, DATASET_TYPE_ARRAY);

        // Does the invitation exist?
        if (!$invitation) {
            throw notFoundException("Invitation");
        }

        // Does this user own the invitation?
        if ($userID != $invitation["InsertUserID"] && !$session->checkPermission("Garden.Moderation.Manage")) {
            throw permissionException("@" . t("InviteErrorPermission", t("ErrorPermission")));
        }

        // Delete it.
        $result = true;
        $this->SQL->delete($this->Name, ["InvitationID" => $id]);
        if (array_key_exists("RowCount", $this->SQL->Database->LastInfo)) {
            // Try to verify we actually did something.
            if ($this->SQL->Database->LastInfo["RowCount"] == 0) {
                $result = false;
            }
        } elseif (!$result) {
            // If our result was falsy, make it official.
            $result = false;
        }

        // Add the invitation back onto the user's account if the invitation has not been accepted.
        if ($result && !$invitation["AcceptedUserID"]) {
            Gdn::userModel()->increaseInviteCount($userID);
        }

        return $result;
    }

    /**
     * Returns a unique 8 character invitation code.
     */
    protected function getInvitationCode()
    {
        // Generate a new invitation code.
        $code = betterRandomString(16, "Aa0");

        // Make sure the string doesn't already exist in the invitation table
        $codeData = $this->getWhere(["Code" => $code]);
        if ($codeData->numRows() > 0) {
            return $this->getInvitationCode();
        } else {
            return $code;
        }
    }
}
